package de.mmis.core.base.abstracttree;

import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;

import de.mmis.core.base.DeserializableAnnotations;
import de.mmis.core.base.Toolbox;
import de.mmis.core.base.abstracttree.LeafNode.Encoding;
import de.mmis.core.base.abstracttree.serializer.DeserializableException;
import de.mmis.core.base.abstracttree.serializer.Serializable;
import de.mmis.core.base.abstracttree.serializer.Serializer;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper;
import de.mmis.core.base.abstracttree.serializer.internal.ClassSerializerMap;
import de.mmis.core.base.abstracttree.serializer.internal.ObjectSerializer;
import de.mmis.core.base.gclass.GClass;

/**
 * Interface representing all types of abstract Tree data as defined in Rivest's
 * Internet draft
 * 
 * @see <a
 *      href="http://people.csail.mit.edu/rivest/Sexp.txt">http://people.csail.mit.edu/rivest/Sexp.txt</a>
 * @author Christian Koch
 * 
 */
public abstract class Tree implements Serializable, DeserializableAnnotations {
	/**
	 * enumeration of all types of abstract Trees
	 * 
	 * @author Christian Koch
	 */
	public enum Type {
		LEAF, INNER,
	}

	/**
	 * @return type of this Tree instance
	 */
	public abstract Type getType();

	public abstract Tree copy(boolean dataCopy);

	// ////////////////////////
	// conversion functions //
	// ////////////////////////

	/**
	 * @param object
	 *            object to be serialized
	 * @retur Tree representing specified object
	 */
	public static Tree fromObject(Object object) {
		return fromObject(object, true, new IdentityHashMap<Object, String>(),
				Tree.class.getClassLoader());
	}

	public static Tree fromObject(Object object, boolean explicit) {
		return fromObject(object, explicit,
				new IdentityHashMap<Object, String>(),
				Tree.class.getClassLoader());
	}

	public static Tree fromObject(Object object, boolean explicit,
			Map<Object, String> refmap, ClassLoader classLoader) {
		return SerializerHelper.serializeObject(object, explicit, refmap,
				classLoader);
	}

	/**
	 * use {@link ObjectSerializer} to generate a {@link Tree} containing the
	 * class name and the name and value of all getter methods of the given
	 * object. Note that this is irreversible. The Java {@link Object} cannot be
	 * reconstructed using the resulting {@link Tree}
	 * 
	 * @param obj
	 *            the object to get get state for
	 * @return a {@link Tree} containing the object state
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public static Tree getObjectState(Object obj) {
		return ((Serializer) ClassSerializerMap.getSerialiser(Object.class,
				Tree.class.getClassLoader())).serialize(obj, false,
				new HashMap<String, Object>(), Tree.class.getClassLoader());
	}

	/**
	 * try to cast this abstract Tree into specified type
	 * 
	 * @param <T>
	 *            class to cast to
	 * @param type
	 *            class to cast to
	 * @return resulting Object
	 * @throws DeserializableException
	 *             cannot cast to specified type
	 */
	public <T> T getAs(GClass type) throws DeserializableException {
		return this.<T> getAs(type, new HashMap<String, Object>());
	}

	public <T> T getAs(Class<T> type) throws DeserializableException {
		return this.<T> getAs(GClass.fromType(type, null));
	}

	public <T> T getAs(GClass type, ClassLoader classLoader)
			throws DeserializableException {
		return this.<T> getAs(type, classLoader, new HashMap<String, Object>());
	}

	public <T> T getAs(Class<T> type, ClassLoader classLoader)
			throws DeserializableException {
		return this.<T> getAs(GClass.fromType(type, null), classLoader);
	}

	/**
	 * try to cast this abstract Tree into specified type
	 * 
	 * @param <T>
	 *            class to cast to
	 * @param type
	 *            class to cast to
	 * @param idmap
	 *            Map of all previously deserialized objects used for resolving
	 *            self-references
	 * @return resulting Object
	 * @throws DeserializableException
	 *             cannot cast to specified type
	 */
	public <T> T getAs(GClass type, Map<String, Object> idmap)
			throws DeserializableException {
		return this.<T> getAs(type, type.getCommonClassLoader(), idmap);
	}

	public <T> T getAs(Class<?> type, Map<String, Object> idmap)
			throws DeserializableException {
		return this.<T> getAs(GClass.fromType(type, null), idmap);
	}

	public <T> T getAs(Class<?> type, ClassLoader classLoader,
			Map<String, Object> idmap) throws DeserializableException {
		return this.<T> getAs(GClass.fromType(type, null), classLoader, idmap);
	}

	@SuppressWarnings("unchecked")
	public <T> T getAs(GClass type, ClassLoader classLoader,
			Map<String, Object> idmap) throws DeserializableException {
		try {
			return (T) SerializerHelper.deserializeObject(this, type,
					classLoader, idmap);
		} catch (ClassCastException e) {
			throw new DeserializableException(this, e);
		}
	}

	/**
	 * check if this abstract Tree can be casted into specified type
	 * 
	 * @param type
	 *            class to cast to
	 * @return true, if able to cast, else false
	 */
	public boolean isClass(Class<?> type) {
		return isClass(GClass.fromType(type, null), true);
	}

	/**
	 * check if this abstract Tree can be casted into specified type
	 * 
	 * @param type
	 *            class to cast to
	 * @param allowNull
	 *            return false if the resulting object would be null
	 * @return true, if able to cast, else false
	 */
	public boolean isClass(Class<?> type, boolean allowNull) {
		return isClass(GClass.fromType(type, null), allowNull);
	}

	/**
	 * check if this abstract Tree can be casted into specified type
	 * 
	 * @param type
	 *            class to cast to
	 * @return true, if able to cast, else false
	 */
	public boolean isClass(GClass type) {
		return isClass(type, true);
	}

	/**
	 * check if this abstract Tree can be casted into specified type
	 * 
	 * @param type
	 *            class to cast to
	 * @param allowNull
	 *            return false if the resulting object would be null
	 * @return true, if able to cast, else false
	 */
	public boolean isClass(GClass type, boolean allowNull) {
		try {
			return (getAs(type) != null || allowNull);
		} catch (DeserializableException e) {
			return false;
		}

	}

	/**
	 * convenience function, try to cast this abstract Tree into a boolean value
	 * 
	 * @return boolean value of this abstract Tree
	 * @throws DeserializableException
	 *             cannot cast to boolean
	 */
	public boolean getAsBoolean() throws DeserializableException {
		return getAs(Boolean.class).booleanValue();
	}

	/**
	 * convenience function, check if this abstract Tree can be casted into a
	 * boolean value
	 * 
	 * @return true, if able to cast, else false
	 */
	public boolean isBoolean() {
		return isClass(Boolean.class, false);
	}

	/**
	 * convenience function, try to cast this abstract Tree into a byte value
	 * 
	 * @return byte value of this abstract Tree
	 * @throws DeserializableException
	 *             cannot cast to byte
	 */
	public byte getAsByte() throws DeserializableException {
		return getAs(Byte.class).byteValue();
	}

	/**
	 * convenience function, check if this abstract Tree can be casted into a
	 * byte value
	 * 
	 * @return true, if able to cast, else false
	 */
	public boolean isByte() {
		return isClass(Byte.class, false);
	}

	/**
	 * convenience function, try to cast this abstract Tree into a short value
	 * 
	 * @return short value of this abstract Tree
	 * @throws DeserializableException
	 *             cannot cast to short
	 */
	public short getAsShort() throws DeserializableException {
		return getAs(Short.class).shortValue();
	}

	/**
	 * convenience function, check if this abstract Tree can be casted into a
	 * short value
	 * 
	 * @return true, if able to cast, else false
	 */
	public boolean isShort() {
		return isClass(Short.class, false);
	}

	/**
	 * convenience function, try to cast this abstract Tree into an int value
	 * 
	 * @return int value of this abstract Tree
	 * @throws DeserializableException
	 *             cannot cast to int
	 */
	public int getAsInt() throws DeserializableException {
		return getAs(Integer.class).intValue();
	}

	/**
	 * convenience function, check if this abstract Tree can be casted into a
	 * int value
	 * 
	 * @return true, if able to cast, else false
	 */
	public boolean isInt() {
		return isClass(Integer.class, false);
	}

	/**
	 * convenience function, try to cast this abstract Tree into a long value
	 * 
	 * @return long value of this abstract Tree
	 * @throws DeserializableException
	 *             cannot cast to long
	 */
	public long getAsLong() throws DeserializableException {
		return getAs(Long.class).longValue();
	}

	/**
	 * convenience function, check if this abstract Tree can be casted into a
	 * long value
	 * 
	 * @return true, if able to cast, else false
	 */
	public boolean isLong() {
		return isClass(Long.class, false);
	}

	/**
	 * convenience function, try to cast this abstract Tree into a float value
	 * 
	 * @return float value of this abstract Tree
	 * @throws DeserializableException
	 *             cannot cast to float
	 */
	public float getAsFloat() throws DeserializableException {
		return getAs(Float.class).floatValue();
	}

	/**
	 * convenience function, check if this abstract Tree can be casted into a
	 * float value
	 * 
	 * @return true, if able to cast, else false
	 */
	public boolean isFloat() {
		return isClass(Float.class, false);
	}

	/**
	 * convenience function, try to cast this abstract Tree into a double value
	 * 
	 * @return double value of this abstract Tree
	 * @throws DeserializableException
	 *             cannot cast to double
	 */
	public double getAsDouble() throws DeserializableException {
		return getAs(Double.class).doubleValue();
	}

	/**
	 * convenience function, check if this abstract Tree can be casted into a
	 * double value
	 * 
	 * @return true, if able to cast, else false
	 */
	public boolean isDouble() {
		return isClass(Double.class, false);
	}

	/**
	 * convenience function, try to cast this abstract Tree into a char value
	 * 
	 * @return char value of this abstract Tree
	 * @throws DeserializableException
	 *             cannot cast to char
	 */
	public char getAsChar() throws DeserializableException {
		return getAs(Character.class).charValue();
	}

	/**
	 * convenience function, check if this abstract Tree can be casted into a
	 * char value
	 * 
	 * @return true, if able to cast, else false
	 */
	public boolean isChar() {
		return isClass(Character.class, false);
	}

	/**
	 * convenience function, try to cast this abstract Tree into an UTF-8
	 * encoded String value
	 * 
	 * @return String value of this abstract Tree
	 * @throws DeserializableException
	 *             cannot cast to String
	 */
	public String getAsString() throws DeserializableException {
		return getAs(String.class);
	}

	/**
	 * convenience function, check if this abstract Tree can be casted into an
	 * UTF-8 encoded String value
	 * 
	 * @return true, if able to cast, else false
	 */
	public boolean isString() {
		return isClass(String.class);
	}

	/**
	 * convenience function, try to cast this abstract Tree into an UTF-8
	 * encoded symbol
	 * 
	 * @return symbol represented by this abstract Tree
	 * @throws DeserializableException
	 *             cannot cast to symbol
	 */
	public String getAsSymbol() throws DeserializableException {
		if (getType() != Type.LEAF
				|| ((LeafNode) this).getEncoding() != Encoding.Token)
			throw new DeserializableException(this, "Tree is no Token atom");

		return ((LeafNode) this).getUTF8Data();
	}

	/**
	 * convenience function, check if this abstract Tree can be casted into an
	 * UTF-8 encoded symbol
	 * 
	 * @return true, if able to cast, else false
	 */
	public boolean isSymbol() {
		try {
			getAsSymbol();
		} catch (DeserializableException e) {
			return false;
		}
		return true;
	}

	/**
	 * converts abstract Tree to String using {@link SimpleTreeWriter} class
	 * 
	 * @return String representation of this abstract Tree
	 */
	@Override
	public String toString() {
		return toString(new SimpleTreeWriter(), false);
	}

	public String toString(TreeWriter abstractTreeWriter) {
		return toString(abstractTreeWriter, false);
	}

	public String toString(TreeWriter abstractTreeWriter,
			boolean escapeNonPrintableCharacters) {
		byte[] buf = abstractTreeWriter.writeAbstractTree(this);
		if (escapeNonPrintableCharacters)
			return byteArrayToString(buf);

		return Toolbox.toUTF8String(buf);
	}

	/**
	 * auxiliary function to convert a byte array to a string escaping not
	 * printable characters
	 * 
	 * @param b
	 *            byte array to be converted
	 * @return String representation of given byte array
	 */
	private static String byteArrayToString(byte[] b) {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < b.length; i++) {
			int bi = (b[i]) & 0xff;
			if (bi == '\\') // escape char
				sb.append("\\\\");
			else if (bi > 0x1F && bi < 0x7F) // printable ascii chars
				sb.append((char) bi);
			else // escape not printable chars
			{
				sb.append('\\');
				sb.append((char) ((bi / 100) + '0'));
				sb.append((char) (((bi % 100) / 10) + '0'));
				sb.append((char) ((bi % 10) + '0'));
			}
		}
		return sb.toString();
	}

	/**
	 * checks whether this abstract Tree matches the specified pattern (wildcard
	 * is ?)
	 * 
	 * @param pattern
	 *            abstract Tree pattern to check against
	 * @return true, if this abstract Tree matches the specified pattern, else
	 *         false
	 */
	public boolean matchPattern(Tree pattern) {
		return this.equals(pattern, true);
	}

	@Override
	public abstract int hashCode();

	@Override
	public boolean equals(Object o) {
		if (!(o instanceof Tree))
			return false;
		return this.equals((Tree) o, false);
	}

	/**
	 * checks whether this abstract Tree is equal to the specified one or
	 * matches the specified pattern
	 * 
	 * @param e
	 *            abstract Tree to check against
	 * @param wildcards
	 *            true, if specified abstract Tree contains wildcards (tokens
	 *            with content ?), that should be respected
	 * @return true, if both abstract Trees are equal (or matching), else false
	 */
	protected abstract boolean equals(Tree e, boolean wildcards);

	@Override
	public String getTypeIdentifier() {
		return "tree";
	}

	@Override
	public Tree[] serialize(boolean explicit, Map<Object, String> refmap,
			ClassLoader classLoader) {
		return new Tree[] { this };
	}

	/**
	 * custom deserialization method simply returning the abstract Tree itself
	 * 
	 * @param e
	 *            the Tree to deserialize
	 * @param idmap
	 *            unused; mapping between references and already deserialized
	 *            objects
	 * @return the deserialized Tree; e in this case
	 * @throws DeserializableException
	 */
	@DeserializationMethod
	public static Object deserialize(Tree[] e, ClassLoader classLoader,
			String ownID, Map<String, Object> idmap)
			throws DeserializableException {
		if (e.length != 1)
			throw new DeserializableException(new InnerNode(e),
					"Tree allows only 1 argument");
		return e[0];
	}
}
